﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Poi.NET.poifs.common;
using Poi.NET.util;

namespace Poi.NET.poifs.storage
{
    /**
     * A block of block allocation table entries. BATBlocks are created
     * only through a static factory method: createBATBlocks.
     *
     * @author Marc Johnson (mjohnson at apache dot org)
     */
    public readonly class BATBlock : BigBlock
    {
        private static readonly int _entries_per_block =
            POIFSConstants.BIG_BLOCK_SIZE / LittleEndianConsts.INT_SIZE;
        private static readonly int _entries_per_xbat_block = _entries_per_block
                                                                - 1;
        private static readonly int _xbat_chain_offset =
            _entries_per_xbat_block * LittleEndianConsts.INT_SIZE;
        private static readonly byte _default_value = (byte)0xFF;
        private IntegerField[] _fields;
        private byte[] _data;

        /**
         * Create a single instance initialized with default values
         */

        private BATBlock()
        {
            _data = new byte[POIFSConstants.BIG_BLOCK_SIZE];
            for (int i = 0; i < _data.Length; i++)
                _data[i] = _default_value;

            _fields = new IntegerField[_entries_per_block];
            int offset = 0;

            for (int j = 0; j < _entries_per_block; j++)
            {
                _fields[j] = new IntegerField(offset);
                offset += LittleEndianConsts.INT_SIZE;
            }
        }

        /**
         * Create an array of BATBlocks from an array of int block
         * allocation table entries
         *
         * @param entries the array of int entries
         *
         * @return the newly created array of BATBlocks
         */

        public static BATBlock[] createBATBlocks(int[] entries)
        {
            int block_count = calculateStorageRequirements(entries.Length);
            BATBlock[] blocks = new BATBlock[block_count];
            int index = 0;
            int remaining = entries.Length;

            for (int j = 0; j < entries.Length; j += _entries_per_block)
            {
                blocks[index++] = new BATBlock(entries, j,
                                                 (remaining > _entries_per_block)
                                                 ? j + _entries_per_block
                                                 : entries.Length);
                remaining -= _entries_per_block;
            }
            return blocks;
        }

        /**
         * Create an array of XBATBlocks from an array of int block
         * allocation table entries
         *
         * @param entries the array of int entries
         * @param startBlock the start block of the array of XBAT blocks
         *
         * @return the newly created array of BATBlocks
         */

        public static BATBlock[] createXBATBlocks(int[] entries,
                                                   int startBlock)
        {
            int block_count =
                calculateXBATStorageRequirements(entries.Length);
            BATBlock[] blocks = new BATBlock[block_count];
            int index = 0;
            int remaining = entries.Length;

            if (block_count != 0)
            {
                for (int j = 0; j < entries.Length; j += _entries_per_xbat_block)
                {
                    blocks[index++] =
                        new BATBlock(entries, j,
                                     (remaining > _entries_per_xbat_block)
                                     ? j + _entries_per_xbat_block
                                     : entries.Length);
                    remaining -= _entries_per_xbat_block;
                }
                for (index = 0; index < blocks.Length - 1; index++)
                {
                    blocks[index].setXBATChain(startBlock + index + 1);
                }
                blocks[index].setXBATChain(POIFSConstants.END_OF_CHAIN);
            }
            return blocks;
        }

        /**
         * Calculate how many BATBlocks are needed to hold a specified
         * number of BAT entries.
         *
         * @param entryCount the number of entries
         *
         * @return the number of BATBlocks needed
         */

        public static int calculateStorageRequirements(int entryCount)
        {
            return (entryCount + _entries_per_block - 1) / _entries_per_block;
        }

        /**
         * Calculate how many XBATBlocks are needed to hold a specified
         * number of BAT entries.
         *
         * @param entryCount the number of entries
         *
         * @return the number of XBATBlocks needed
         */

        public static int calculateXBATStorageRequirements(int entryCount)
        {
            return (entryCount + _entries_per_xbat_block - 1)
                   / _entries_per_xbat_block;
        }

        /**
         * @return number of entries per block
         */

        public static readonly int entriesPerBlock()
        {
            return _entries_per_block;
        }

        /**
         * @return number of entries per XBAT block
         */

        public static readonly int entriesPerXBATBlock()
        {
            return _entries_per_xbat_block;
        }

        /**
         * @return offset of chain index of XBAT block
         */

        public static readonly int getXBATChainOffset()
        {
            return _xbat_chain_offset;
        }

        private void setXBATChain(int chainIndex)
        {
            _fields[_entries_per_xbat_block].set(chainIndex, _data);
        }

        /**
         * Create a single instance initialized (perhaps partially) with entries
         *
         * @param entries the array of block allocation table entries
         * @param start_index the index of the first entry to be written
         *                    to the block
         * @param end_index the index, plus one, of the last entry to be
         *                  written to the block (writing is for all index
         *                  k, start_index <= k < end_index)
         */

        private BATBlock(int[] entries, int start_index,
                         int end_index)
        {
            _data = new byte[POIFSConstants.BIG_BLOCK_SIZE];
            for (int i = 0; i < _data.Length; i++)
                _data[i] = _default_value;

            _fields = new IntegerField[_entries_per_block];
            int offset = 0;

            for (int j = 0; j < _entries_per_block; j++)
            {
                _fields[j] = new IntegerField(offset);
                offset += LittleEndianConsts.INT_SIZE;
            }

            for (int k = start_index; k < end_index; k++)
            {
                _fields[k - start_index].set(entries[k], _data);
            }
        }

        /* ********** START extension of BigBlock ********** */

        /**
         * Write the block's data to an OutputStream
         *
         * @param stream the OutputStream to which the stored data should
         *               be written
         *
         * @exception IOException on problems writing to the specified
         *            stream
         */

        void writeData(Stream stream)
        {
            doWriteData(stream, _data);
        }

        /* **********  END  extension of BigBlock ********** */
    }   // end public class BATBlock
}
